package xj.graph2d.graph;

/**
 * A class used for representing attributes associated with the graph elements.
 * An attribute consists of a name-value pair and an element type. Once an
 * attribute is constructed, the name cannot be changed. The element type and
 * the attribute name are used in determining how a string representation of an
 * attribute value is to be converted to an Object and vice versa. The Element
 * class method setUserAttributeType allows users to take advantage of Grappa's
 * built-in converters or to pass a (negative) integer as a conversion indicator
 * to a user-supplied AttributeHandler.
 * 
 */
public final class Attribute extends java.util.Observable implements
GraphConstants, AttributeHandler {

  // the custom attribute handler
  public static AttributeHandler attrHandler = null;

  /**
   * Set a custom attribute handler for converting a String value to an
   * Object and vice versa.
   * 
   * @param newHandler
   *                the AttributeHandler to use for conversions
   * 
   * @return the previously set AttributeHandler or null
   * 
   * @see AttributeHandler
   */
  public static AttributeHandler setAttributeHandler(AttributeHandler newHandler) {
    AttributeHandler oldHandler = attrHandler;
    attrHandler = newHandler;
    return oldHandler;
  }

  private static boolean debug = true;

  // attribute name
  private String name;

  // attribute value as a string
  private String stringValue;

  // attribute string value converted to an object based on its type
  private Object value;

  // the element type associated with this attribute (includes system)
  private int elementType;

  // the attribute type for conversion to/from a string
  private int attributeType;

  // the hash value of the attribute name
  private int nameHash;

  /**
   * Constructs a new attribute from a name / value pair.
   * 
   * @param elemType
   *                the element type with which the attribute is or will
   *                be associated.
   * @param attrName
   *                the name of the attribute.
   * @param attrValue
   *                the value of the attribute.
   * 
   */
  public Attribute(int elemType, String attrName, Object attrValue) {
    super();

    if (attrName == null) {
      throw new IllegalArgumentException(
      "the name of an Attribute pair cannot be null");
    }
    attributeType = attributeType(elemType, attrName);
    elementType = elemType;
    name = attrName;
    nameHash = name.hashCode();
    setValue(attrValue);

    if (debug) {
      System.out.print("Attribute(): ");
      switch (elemType) {
      case NODE:
	System.out.print("NODE, ");
	break;
      case EDGE:
	System.out.print("EDGE, ");
	break;
      case SUBGRAPH:
	System.out.print("SUBGRAPH, ");
	break;
      case SYSTEM:
	System.out.print("SYSTEM, ");
	break;
      default:
	System.out.print("" + elemType + ", ");
      break;
      }
      System.out.println(attrName + ", " + attrValue);
      System.out.println("   value=" + value + "   stringValue=" + stringValue);
    }

  }

  /**
   * Constructs a new attribute from an existing one.
   * 
   * @param attr
   *                the attribute from which this new one is to be
   *                generated
   */
  public Attribute(Attribute attr) {
    this(attr.getElementType(), attr.getName(), attr.getValue());
  }

  /**
   * Get the element type for this attribute.
   * 
   * @return the element type for this attribute
   * 
   */
  public final int getElementType() {
    return elementType;
  }

  /**
   * Get the attribute value type for this attribute.
   * 
   * @return the attribute value type for this attribute.
   */
  public final int getAttributeType() {
    return attributeType;
  }

  /**
   * Get the name of this attribute.
   * 
   * @return the name of this attribute.
   */
  public final String getName() {
    return name;
  }

  /**
   * Get the value of this attribute.
   * 
   * @return the value of the attribute.
   */
  public final Object getValue() {
    if (value == null && stringValue != null) {
      value = convertStringValue(elementType, name, stringValue, attributeType);
    }
    return value;
  }

  /**
   * Get the value of this attribute converted to a String.
   * 
   * @return the value of the attribute as a String.
   */
  public final String getStringValue() {
    /*
     * switch(attributeType) { // put the types here that users might change
     * on their own // after doing a getValue() so that we always recompute //
     * the string value when it is requested case HASHLIST_TYPE: stringValue =
     * null; break; }
     */
    if (stringValue == null && value != null) {
      stringValue = convertValue(elementType, name, value, attributeType);
    }
    return stringValue;
  }

  /**
   * Set the value of the attribute. If the value is different than the
   * current value, the Observable changed indicator is set.
   * 
   * @param attrValue
   *                the new attribute value.
   * @return the old attribute value.
   */
  public final Object setValue(Object attrValue) {
    boolean changed = false;
    boolean isString = false;
    Object oldValue = null;
    if (attrValue != null && attrValue instanceof String) {
      isString = true;
      oldValue = getStringValue();
      attrValue = ((String) attrValue).trim();
    } else {
      oldValue = getValue();
    }
    // note: since we have called either getValue() or getStringValue(),
    // both value and stringValue are up-to-date
    if (attrValue != null) {
      if (isString) {
	if (changed = (stringValue == null || !attrValue.equals(stringValue))) {
	  stringValue = (String) attrValue;
	  value = null;
	}
      } else {
	if (changed = (value == null || !attrValue.equals(value))) {
	  value = copyValue(elementType, name, attrValue, attributeType);
	  stringValue = null;
	}
      }
    } else {
      if (changed = (value != null)) {
	value = null;
	stringValue = null;
      }
    }
    if (changed) {
      setChanged();
    }
    return oldValue;
  }

  /**
   * Tests for equality with the given attribute.
   * 
   * @param attr
   *                the attribute with which to compare this attribute.
   * @return true if the two attributes are equal, false otherwise.
   */
  public final boolean equals(Attribute attr) {
    if (attr == null) {
      return false;
    }
    if (this == attr) {
      return true;
    }
    if (nameHash != attr.getNameHash() || !attr.getName().equals(name)) {
      return false;
    }
    String attrValue = attr.getStringValue();
    if (attrValue == getStringValue()) {
      return true;
    }
    if (attrValue == null) {
      return false;
    }
    // note: since getStringValue() was called, stringValue is up-to-date
    return attrValue.equals(stringValue);
  }

  /**
   * Tests for equality of this attribute's value with the given
   * attribute's value. The attribute names are not compated.
   * 
   * @param attr
   *                the attribute with which to compare this attribute.
   * @return true if the two attribute values are equal, false otherwise.
   */
  public final boolean equalsValue(Attribute attr) {
    if (attr == null) {
      return false;
    }
    if (this == attr) {
      return true;
    }
    String attrValue = attr.getStringValue();
    if (attrValue == getStringValue()) {
      return true;
    }
    if (attrValue == null) {
      return false;
    }
    // note: since getStringValue() was called, stringValue is up-to-date
    return attrValue.equals(stringValue);
  }

  /**
   * Get the hash value for this attributes name.
   * 
   * @return the hash code for the name portion of this attribute
   */
  public final int getNameHash() {
    return nameHash;
  }

  /**
   * Use to indicate that this object has changed. This method is a
   * convenience method that calls the corresponding protected method of
   * the Observable class.
   * 
   * @see java.util.Observable#setChanged()
   */
  @Override
  public final void setChanged() {
    super.setChanged();
  }

  /**
   * Use to indicate that this object has no longer changed, or that it
   * has already notified all of its observers of its most recent change.
   * This method is a convenience method that calls the corresponding
   * protected method of the Observable class.
   * 
   * @see java.util.Observable#clearChanged()
   */
  @Override
  public final void clearChanged() {
    super.clearChanged();
  }

  /**
   * Provide a generic string representation of the attribute.
   */
  @Override
  public String toString() {
    return getClass().getName() + "[name=\"" + name + "\",value=\""
    + getStringValue() + "\",stringValue=\"" + stringValue + "\"]";
  }

  /**
   * Returns the attribute conversion type for the supplied attribute name
   * and element type..
   * 
   * @param elemType
   *                the element type
   * @param attrName
   *                the attribute name
   * @return the currently associated attribute type
   * 
   */
  public int attributeType(int elemType, String attrName) {
    if (attrHandler != null) {
      return attrHandler.attributeType(elemType, attrName);
    } else {
      return 0;
    }
  }

  /**
   * Convert the supplied value to a string. How to convert the value is
   * based on the type, name and attrtype information supplied. Note: this
   * method really could be declared static except that it hides the
   * instance method declared in the AttributeHandler interface and a
   * class method cannot hide an instance method.
   * 
   * @param elemType
   *                the element type to which the named attribute applies
   * @param name
   *                the name of the attribute
   * @param value
   *                the object value to be converted to a string
   * @param convType
   *                the object-to-string conversion type of the value
   *                object
   * @return a string representation of the supplied value
   */
  public String convertValue(int elemType, String name, Object value,
      int convType) {
    if (attrHandler != null) {
      return attrHandler.convertValue(elemType, name, value, convType);
    } else {
      return null;
    }
  }

  /**
   * Convert the supplied string value to the appropriate Object. How to
   * convert the value is based on the type, name and attrtype information
   * supplied. Note: this method really could be declared static except
   * that it hides the instance method declared in the AttributeHandler
   * interface and a class method cannot hide an instance method.
   * 
   * @param elemType
   *                the element type to which the named attribute applies
   * @param name
   *                the name of the attribute
   * @param value
   *                the string value to be converted to an object
   * @param convType
   *                the string-to-object conversion type of the value
   *                object
   * @return an object representation of the supplied value
   */
  public Object convertStringValue(int elemType, String name,
      String stringValue, int convType) {
    if (attrHandler != null) {
      return attrHandler.convertStringValue(elemType, name, stringValue,
	  convType);
    } else {
      return null;
    }
  }

  /**
   * Make a copy of the supplied value. How to copy the value is based on
   * the type, name and attrtype information supplied. Note: this method
   * really could be declared static except that it hides the instance
   * method declared in the AttributeHandler interface and a class method
   * cannot hide an instance method.
   * 
   * @param elemType
   *                the element type to which the named attribute applies
   * @param name
   *                the name of the attribute
   * @param value
   *                the attribute value to be copied
   * @param convType
   *                the conversion type of the value object
   * @return a copy of the supplied value
   */
  public Object copyValue(int elemType, String name, Object value, int convType) {
    if (attrHandler != null) {
      return attrHandler.copyValue(elemType, name, value, convType);
    } else {
      return null;
    }
  }

}
